iT邦幫忙

2025 iThome 鐵人賽

DAY 6
1
Mobile Development

《30 天 Flutter:跨平台 AI 行程規劃 App》系列 第 6

Day 6 - 從 Figma 到 Flutter:將設計系統化為 UI 元件

  • 分享至 

  • xImage
  •  

在 Figma 中將設計元素整理成系統與元件後,現在我們要進入更關鍵的一步:將這些視覺藍圖化為實際的 Flutter 程式碼!這不只是將設計稿變成 App 畫面,更是透過程式碼的「元件化」,讓未來的開發工作更有效率、程式碼更乾淨、也更易於維護。

建立資料夾結構

在開始撰寫程式碼之前,先建立一個清晰的資料夾結構,能避免所有程式碼都擠在 main.dart 中,不僅有助於維護,也讓專案架構更有條理,以下是此範例將使用的資料夾架構:

lib/
├── main.dart                # App 進入點,負責載入主題與啟動整個應用程式
│
├── components/              # 可重複使用的 UI 元件
│   ├── buttons/             # 按鈕元件集合
│   │   ├── primary_button.dart  # 主要按鈕樣式(品牌色)
│   │   └── selection_button.dart     # 輕量化的 Chip 樣式按鈕
│   │
│   ├── inputs/              # 輸入相關元件
│   │   └── text_field.dart      # 自訂輸入框
│   │
│   ├── navs/                # 導覽列相關元件
│   │   └── app_bar.dart         # 自訂 AppBar 樣式
│
└── theme/                   # 設計系統相關設定
    ├── app_colors.dart          # 定義應用程式的顏色系統
    ├── app_typography.dart      # 定義字體、字重、字級等文字樣式
    ├── app_spacing.dart         # 定義間距單位(如 4、8、16…)
    ├── app_radius.dart          # 定義圓角大小,確保元件邊角一致
    └── app_theme.dart           # 將以上設定組合成 ThemeData,提供全局使用

為什麼要元件化?

元件化的核心思想是「單一職責」,也就是讓每個 Widget 都專注於一個特定的功能或視覺元素。這樣做的好處非常多:

  • 提高開發效率:未來遇到相同的 UI,可以直接呼叫元件,省下大量的重複開發時間。
  • 確保視覺一致性:所有元件都依據設計系統規範來建構,無論在 App 的哪個頁面使用,它們的視覺效果(顏色、字型、間距)都會保持一致。
  • 易於維護:當設計系統有更動時(例如按鈕的圓角變更),你只需要修改主元件的程式碼,所有使用該元件的地方都會自動更新,大大降低維護成本。

元件設計前的考量與注意事項

在設計與實作元件時,有幾個重要的點需要考量:

  • 屬性參數化:思考元件的哪些部分是可變的?將這些部分定義為參數,例如按鈕的文字、顏色,或 App Bar 的標題。
  • 狀態管理:元件通常有多種狀態,例如按鈕的「啟用」和「禁用」狀態。你需要在程式碼中處理這些狀態的邏輯,例如當 onPressednull 時,改變按鈕顏色並禁用點擊。
  • 設計系統串接:避免在元件程式碼中手動輸入顏色碼或字體大小。請務必使用 Theme.of(context) 或自定義的 ThemeExtension 來獲取設計系統中的值,這能確保你的程式碼與設計規範保持同步。
  • 註解:為元件寫下清晰的註解,說明它的用途、接受的參數以及如何使用,這對協同開發非常重要。
  • 單一職責:每個元件都應該只做一件事。例如,一個按鈕元件應該只處理按鈕的樣式和點擊行為,而不應該包含複雜的商業邏輯。
  • 可預測性:一個好的元件應該是可預測的。當你傳入相同的參數時,它應該始終產生相同的視覺效果。將所有影響顯示的變數都作為參數傳入,避免直接使用外部狀態和隨機函式。

實作 PrimaryButton 元件

將以「主要按鈕(PrimaryButton)」為例,從零開始建構一個完整的 Flutter 元件。這個元件的樣式將會與我們之前建立的設計系統(顏色、間距、圓角、字型)緊密結合。

  1. 定義屬性 (Props):
    一個好的元件應該具備足夠的靈活性。我們需要思考一個按鈕可能有哪些變化,並將它們定義為元件的屬性。
  • text:按鈕上顯示的文字。
  • onPressed:按鈕被點擊時執行的動作。當此屬性為 null 時,按鈕會自動變成禁用狀態。
  • size:按鈕的尺寸,例如 mediumlarge
  • leftIconrightIcon:可選的左右圖示,讓按鈕有更多樣的組合。
  1. 導入設計系統的數值:
    為了保持與設計系統的一致,我們不會在程式碼中寫死任何數值。本次實作的關鍵在於,按鈕的樣式將直接從我們定義好的靜態類別中取得,包含:
  • 顏色:
    • 方法:直接引用 AppPrimaryColors.primary100AppGrayscaleColors.gray200 等靜態屬性。
    • 優點:這種做法確保了按鈕的顏色完全符合設計規範,並且集中管理,未來如果需要調整顏色,只需修改 app_colors.dart 檔案即可。
  • 間距:
    • 方法:透過 AppSpacing 類別取得間距值,例如 AppSpacing.small 來控制圖示與文字之間的距離。
    • 優點:這樣能確保所有元件的內部間距都是標準化且一致的。
  • 圓角:
    • 方法:從 AppRadius 類別取得圓角值,例如 AppRadius.medium 來設定按鈕的圓角。
    • 優點:確保所有可互動元件的圓角風格都是統一的。
  • 字型:
    • 方法:利用 AppTypography.textTheme 來取得預先定義好的文字樣式,例如 Theme.of(context).textTheme.bodyLarge
    • 優點:確保文字的字重、大小、行高都符合設計規範,無需手動調整。
  1. 處理不同狀態的樣式:
    按鈕有「啟用」與「禁用」兩種主要狀態,它們的樣式(如顏色)應該有所不同。
  • 啟用:背景色為 AppPrimaryColors.primary100,文字和圖示顏色為 AppGrayscaleColors.gray800
  • 禁用:背景色為 AppGrayscaleColors.gray200,文字和圖示顏色為 AppGrayscaleColors.gray300
  1. 使用方式:
    接下來就可以在 App 的任何地方輕鬆地呼叫它~
  • 基本使用:
PrimaryButton(
  text: '點我',
  onPressed: () {
    // 點擊後執行的動作
  },
)
  1. 完整程式碼:
class PrimaryButton extends StatelessWidget {
  // 按鈕上顯示的文字
  final String text;
  // 按鈕被點擊時觸發的函式
  // 如果傳入 null,按鈕會呈現禁用狀態
  final VoidCallback? onPressed;
  // 按鈕的尺寸,使用上面定義的枚舉
  final AppButtonSize size;
  // 可選的左邊圖示
  final IconData? leftIcon;
  // 可選的右邊圖示
  final IconData? rightIcon;

  const PrimaryButton({
    super.key,
    required this.text,
    this.onPressed,
    this.size = AppButtonSize.medium,
    this.leftIcon,
    this.rightIcon,
  });

  @override
  Widget build(BuildContext context) {
    // 判斷按鈕是否為禁用狀態
    final bool isDisabled = onPressed == null;

    // 根據尺寸枚舉設定填充、最小高度和圖示大小
    final EdgeInsets padding;
    final double minHeight;
    final double spacing;
    final double iconSize;

    switch (size) {
      case AppButtonSize.medium:
        padding = const EdgeInsets.symmetric(horizontal: AppSpacing.medium);
        minHeight = 36.0;
        spacing = AppSpacing.small;
        iconSize = 20.0;
        break;
      case AppButtonSize.large:
        padding = const EdgeInsets.symmetric(horizontal: AppSpacing.medium);
        minHeight = 48.0;
        spacing = AppSpacing.medium;
        iconSize = 24.0;
        break;
    }

    // 統一設定背景色與文字/圖示顏色
    final Color backgroundColor =
        isDisabled ? AppGrayscaleColors.gray200 : AppPrimaryColors.primary100;
    final Color foregroundColor =
        isDisabled ? AppGrayscaleColors.gray300 : AppGrayscaleColors.gray800;

    // 定義按鈕的樣式
    final ButtonStyle buttonStyle = ElevatedButton.styleFrom(
      // 背景色
      backgroundColor: backgroundColor,
      // 文字和圖示顏色
      foregroundColor: foregroundColor,
      // 圓角半徑
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(AppRadius.medium),
      ),
      // 內邊距
      padding: padding,
      // 陰影效果
      elevation: 0,
      // 定義按鈕的最小高度
      minimumSize: Size.fromHeight(minHeight),
    );

    // 建立按鈕的文字和圖示
    final Widget buttonContent = Row(
      mainAxisSize: MainAxisSize.min, // 讓 Row 的寬度只包住內容
      children: [
        // 如果有左邊圖示就顯示
        if (leftIcon != null) ...[
          Icon(
            leftIcon,
            color: foregroundColor,
            size: iconSize,
          ),
          SizedBox(width: spacing), // 圖示和文字之間的間距
        ],
        // 按鈕文字
        Text(
          text,
          style: Theme.of(context).textTheme.bodyLarge!.copyWith(
                color: foregroundColor,
              ),
        ),
        // 如果有右邊圖示就顯示
        if (rightIcon != null) ...[
          SizedBox(width: spacing), // 圖示和文字之間的間距
          Icon(
            rightIcon,
            color: foregroundColor,
            size: iconSize,
          ),
        ],
      ],
    );

    return ElevatedButton(
      // 只有在啟用時才會有 onPressed 效果
      onPressed: isDisabled ? null : onPressed,
      style: buttonStyle,
      child: buttonContent,
    );
  }
}

今日成果

按鈕 輸入框
https://ithelp.ithome.com.tw/upload/images/20250820/20178195hUVlwqOAEL.png

在 Day 6 的 PrimaryButton 實作中,我們採用了靜態顏色類別來定義樣式。然而,這種做法並非 Flutter 推薦的「主題化」最佳實踐。

為了讓設計系統更加健壯,同時能夠輕鬆支援淺色、深色等多種顏色模式,將在 Day 7 正式導入多主題設計。透過這種方式,不僅能讓 UI 具備更強的適應性,也能大幅提升程式碼的彈性和可維護性。


上一篇
Day 5 - 一樣的地方改一次就好:在 Figma 中打造專屬元件庫
下一篇
Day 7 - 不只是深色模式:讓你的 Flutter App 也有個性主題
系列文
《30 天 Flutter:跨平台 AI 行程規劃 App》21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言